package cn.bincker.mycaptcha.letter;

import cn.bincker.mycaptcha.CacheManager;
import cn.bincker.mycaptcha.basic.CaptchaBackgroundCutter;
import cn.bincker.mycaptcha.basic.entity.MyQuestion;
import cn.bincker.mycaptcha.basic.impl.AbstractCaptchaFactory;
import cn.bincker.mycaptcha.basic.properties.PaddingProperties;
import cn.bincker.mycaptcha.letter.entity.LetterAnswer;
import cn.bincker.mycaptcha.letter.entity.LetterCaptcha;
import cn.bincker.mycaptcha.utils.FontUtils;
import cn.bincker.mycaptcha.utils.ImageUtils;
import cn.bincker.mycaptcha.utils.RandomUtils;
import com.jhlabs.image.GradientWipeFilter;
import com.jhlabs.image.InvertFilter;
import lombok.Getter;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.InvalidPropertyException;

import java.awt.*;
import java.awt.geom.AffineTransform;
import java.awt.image.BufferedImage;
import java.util.Arrays;
import java.util.stream.Collectors;

@Slf4j
public class LetterCaptchaFactory extends AbstractCaptchaFactory<LetterCaptcha, LetterAnswer> {
    private final LetterCaptchaProperties properties;
    private final CacheManager<LetterCaptcha, LetterAnswer> cacheManager;
    private final String[] fonts;

    protected LetterCaptchaFactory(LetterCaptchaProperties properties, CaptchaBackgroundCutter backgroundCutter, CacheManager<LetterCaptcha, LetterAnswer> cacheManager) {
        super(properties, backgroundCutter);
        this.properties = properties;
        this.cacheManager = cacheManager;
        fonts = properties.getFonts();
        if(fonts.length < 1){
            throw new InvalidPropertyException(Font[].class, "my-captcha.letter.fonts", "请设置字体");
        }
        var availableFontFamilyNames = Arrays.stream(FontUtils.getCanDisplayFontFamilyNames())
                .collect(Collectors.toSet());
        for (String font : fonts) {
            if(!availableFontFamilyNames.contains(font)){
                log.error("有效字体列表: \n{}", String.join("\n", availableFontFamilyNames));
                throw new InvalidPropertyException(Font[].class, "my-captcha.letter.fonts", "无效字体:" + font);
            }
        }
    }

    @Override
    public MyQuestion<LetterCaptcha, LetterAnswer> build() {
        var captchaImage = new BufferedImage(properties.getWidth(), properties.getHeight(), BufferedImage.TYPE_INT_RGB);
        var background = buildBackground();

        var captcha = new LetterCaptcha();
        var answer = new LetterAnswer(RandomUtils.randomString(properties.getLetterNumber()));
        var graphics = captchaImage.createGraphics();

        log.debug("captcha answer is: {}", answer.getCode());

        //绘制背景
        graphics.drawImage(background, 0, 0, null);

        var letterImage = buildLetterImage(answer, background);
        graphics.drawImage(letterImage, 0, 0, captchaImage.getWidth(), captchaImage.getHeight(), 0, 0, letterImage.getWidth(), letterImage.getHeight(), null);

        graphics.dispose();
        var question = new MyQuestion<>(captcha, answer);
        output(question, captchaImage);
        return question;
    }

    private BufferedImage buildLetterImage(LetterAnswer answer, BufferedImage background) {
        var result = new BufferedImage(properties.getWidth(), properties.getHeight(), BufferedImage.TYPE_INT_ARGB);
        var graphics = result.createGraphics();

        //noinspection MagicConstant
        var letterInfoList = answer.getCode()
                .chars()
                .mapToObj(c-> new LetterInfo(
                        (char)c,
                        new Font(
                                fonts[random.nextInt(fonts.length)],
                                random.nextInt(4),
                                RandomUtils.nextInt(properties.getMinFontSize(), properties.getMaxFontSize())
                        ),
                        graphics
                ))
                .collect(Collectors.toList());

        var lettersWidth = letterInfoList.stream().mapToInt(LetterInfo::getWidth).sum();
        var padding = properties.getPadding();
        //字符间隔
        var letterSpacing = (properties.getWidth() - padding.getLeft() - padding.getRight() - lettersWidth) / (letterInfoList.size() - 1);
        //开始位置
        var startLeft = padding.getLeft();
        //如果计算的字符间隔大于设置的字符间隔，说明足够宽，重新计算开始位置，使其居中，并将字符间隔设置为用户设置的字符间隔
        if(letterSpacing > properties.getLetterSpacing()){
            letterSpacing = properties.getLetterSpacing();
            startLeft = padding.getLeft() + (properties.getWidth() - padding.getLeft() - padding.getRight() - lettersWidth - letterSpacing * (letterInfoList.size() - 1)) / 2;
        }
        var minXOffset = properties.getMinXOffset();
        var maxXOffset = properties.getMaxXOffset();
        var minYOffset = properties.getMinYOffset();
        var maxYOffset = properties.getMaxYOffset();
        for (int i = 0, left = startLeft; i < letterInfoList.size(); i++) {
            var letterInfo = letterInfoList.get(i);
            var x = left + RandomUtils.nextInt(-minXOffset, maxXOffset);
            var y = randomY(padding, minYOffset, maxYOffset, letterInfo);
            var randomAngle = RandomUtils.nextInt(properties.getMinRotateAngle(), properties.getMaxRotateAngle());
            switch (random.nextInt(2)){
                //随机颜色
                case 0:
                    drawLetterBySingleColor(
                            graphics,
                            letterInfo,
                            x,
                            y,
                            randomAngle,
                            RandomUtils.randomColor()
                    );
                    break;
                //反色
                case 1:
                    drawLetterByInvertImage(graphics, letterInfo, x, y, randomAngle, background, result);
                    break;
            }
            left += letterInfo.width + letterSpacing;
        }

        graphics.dispose();
        return result;
    }

    /**
     * 绘制反色的字符
     */
    private void drawLetterByInvertImage(Graphics2D graphics, LetterInfo letterInfo, double x, double y, int angle, BufferedImage background, BufferedImage letterImage) {
        var maskBufferedImage = new BufferedImage(properties.getWidth(), properties.getHeight(), BufferedImage.TYPE_INT_ARGB);
        var maskGraphics = maskBufferedImage.createGraphics();
        drawLetterBySingleColor(maskGraphics, letterInfo, x, y, angle, Color.WHITE);
        maskGraphics.dispose();

        var filter = new GradientWipeFilter();
        var dist = new BufferedImage(properties.getWidth(), properties.getHeight(), BufferedImage.TYPE_INT_ARGB);
        var distGraphics = dist.createGraphics();
        filter.setMask(maskBufferedImage);
        filter.setDensity(0.5f);
        filter.setSoftness(1);
        //随机对已经绘制的字母也进行翻转
        if(random.nextBoolean()){
            var currentBackground = new BufferedImage(properties.getWidth(), properties.getHeight(), BufferedImage.TYPE_INT_ARGB);
            var currentBackgroundGraphics = currentBackground.createGraphics();
            currentBackgroundGraphics.drawImage(background, 0, 0, null);
            currentBackgroundGraphics.drawImage(letterImage, 0, 0, null);
            currentBackgroundGraphics.dispose();
            background = currentBackground;
        }
        distGraphics.drawImage(background, filter, 0, 0);
        distGraphics.dispose();

        graphics.setTransform(new AffineTransform());
        graphics.drawImage(dist, new InvertFilter(), 0, 0);
    }

    /**
     * 绘制单色字符
     */
    private void drawLetterBySingleColor(Graphics2D graphics, LetterInfo letterInfo, double x, double y, int angle, Color color) {
        double radian = angle / 180d;
        graphics.setTransform(new AffineTransform());
        graphics.setColor(color);
        graphics.setFont(letterInfo.getFont());

        var isStroke = properties.getEnableStroke() && random.nextBoolean();
        var isHollowOut = properties.getHollowOut() && isStroke && random.nextBoolean();
        graphics.translate(x, y);
        graphics.rotate(radian);
        graphics.drawString(letterInfo.c, 0, 0);
        if(isStroke){
            graphics.setStroke(new BasicStroke(properties.getStrokeWidth()));
            graphics.draw(letterInfo.getShape());

            if(isHollowOut){
                graphics.translate(
                        RandomUtils.nextInt(-properties.getHollowOutMinOffsetX(), properties.getHollowOutMaxOffsetX()),
                        RandomUtils.nextInt(-properties.getHollowOutMinOffsetY(), properties.getHollowOutMaxOffsetY())
                );
                graphics.setComposite(AlphaComposite.Clear);
                graphics.setStroke(new BasicStroke(1));
                graphics.fill(letterInfo.getShape());
                graphics.setComposite(AlphaComposite.SrcOver);
            }
        }
    }

    /**
     * 随机纵坐标
     * @param padding 边距
     * @param minYOffset 最小纵坐标偏移
     * @param maxYOffset 最大纵坐标偏移
     * @param letterInfo 字符信息
     */
    private double randomY(PaddingProperties padding, Integer minYOffset, Integer maxYOffset, LetterInfo letterInfo) {
        var y = properties.getHalfHeight() + RandomUtils.nextInt(-minYOffset, maxYOffset);
        if(y < padding.getTop() + letterInfo.getHalfSize()){
            y = padding.getTop() + letterInfo.getHalfSize();
        }
        if(y > properties.getHeight() - padding.getBottom() - letterInfo.getHalfSize()){
            y = properties.getHeight() - padding.getBottom() - letterInfo.getHalfSize();
        }
        return y + letterInfo.getHalfSize();
    }

    /**
     * 画字符
     */
    private void drawLetter(Graphics2D graphics, LetterInfo letterInfo) {

    }

    private void output(MyQuestion<LetterCaptcha, LetterAnswer> question, BufferedImage captchaImage) {
        if(properties.getCache().getEnabled()) {
            cacheManager.put(question, captchaImage);
        }else{
            question.getCaptcha().setImage(ImageUtils.convertImageToBase64(captchaImage, "jpg"));
        }
    }

    @Getter
    private static class LetterInfo{
        private final String c;
        private final Font font;
        private final int width;
        private final double halfWidth;
        private final int size;
        private final double halfSize;
        private final int height;
        private final double halfHeight;
        private final Shape shape;

        private LetterInfo(char c, Font font, Graphics2D graphics) {
            this.c = String.valueOf(c);
            this.font = font;
            graphics.setFont(font);
            var fontMetrics = graphics.getFontMetrics(font);
            width = fontMetrics.charWidth(c);
            halfWidth = width / 2d;
            height = fontMetrics.getHeight();
            halfHeight = height / 2d;
            size = font.getSize();
            halfSize = size / 2d;
            shape = font.createGlyphVector(graphics.getFontRenderContext(), this.c).getOutline();
        }
    }
}
